﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace EvoBoost.Core.Evolutionary
{
	public class Population<T> where T : class, IChromosome<T>
	{
		private readonly int populationSize;

		private static readonly Random random = new Random();

		private List<T> chromosomes;

		private ReadOnlyCollection<T> chromosomesReadOnly;

		private int epochPassed;

		private int mutationsHappened;

		private int crossoversHappened;

		private double crossoverRate;

		private double mutationRate;

		private double maxFitness;

		private double averageFitness;

		private T bestChromosome;

		public Population(T ancestor, int populationSize)
		{
			if (ancestor == null) throw new ArgumentNullException("ancestor");
			if (populationSize <= 0) throw new ArgumentOutOfRangeException("populationSize", "Population size should be positive.");

			this.mutationRate = 0.2;
			this.crossoverRate = 0.7;

			this.populationSize = populationSize;

			this.GenerateInitialPopulation(ancestor);
			this.CalculateStatistics();
		}

		public int EpochPassed
		{
			get { return this.epochPassed; }
		}

		public int MutationsHappened
		{
			get { return this.mutationsHappened; }
		}

		public int CrossoversHappened
		{
			get { return this.crossoversHappened; }
		}

		public int PopulationSize
		{
			get { return this.populationSize; }
		}

		public double MutationRate
		{
			get { return this.mutationRate; }
			set
			{
				if (value <= 0 || value >= 1)
					throw new ArgumentOutOfRangeException("value", "Mutation rate should lie between 0 and 1.");
				this.mutationRate = value;
			}
		}

		public double CrossoverRate
		{
			get { return this.crossoverRate; }
			set
			{
				if (value <= 0 || value >= 1)
					throw new ArgumentOutOfRangeException("value", "Crossover rate should lie between 0 and 1.");
				this.crossoverRate = value;
			}
		}

		public double MaxFitness
		{
			get { return this.maxFitness; }
		}

		public double AverageFitness
		{
			get { return this.averageFitness; }
		}

		public T BestChromosome
		{
			get { return this.bestChromosome; }
		}

		public ReadOnlyCollection<T> Chromosomes
		{
			get { return this.chromosomesReadOnly; }
		}

		public void RunEpoch()
		{
			this.PerformCrossover();
			this.PerformMutation();
			this.PerformSelection();
			this.CalculateStatistics();
			Debug.Assert(this.chromosomes.Count == this.populationSize);

			this.epochPassed += 1;
		}

		private void CalculateStatistics()
		{
			Func<T, double> chromosomeToFitness = ch => ch.Fitness;

			this.maxFitness = this.chromosomes.Max(chromosomeToFitness);
			this.averageFitness = this.chromosomes.Average(chromosomeToFitness);
			this.bestChromosome = this.chromosomes.Find(ch => ch.Fitness == this.maxFitness);
		}

		private void PerformSelection()
		{
			// Recalculate fitness (in parallel)
			Parallel.ForEach(this.chromosomes, c => { double fitnessAccess = c.Fitness; });

			// Selection strategy with ellitism
			this.chromosomes.Sort((ch1, ch2) => Math.Sign(ch2.Fitness - ch1.Fitness));
			if (this.chromosomes.Count > this.populationSize)
				this.chromosomes.RemoveRange(this.populationSize, this.chromosomes.Count - this.populationSize);
		}

		private void PerformMutation()
		{
			for (int i = 0; i < this.populationSize; ++i)
			{
				if (random.NextDouble() > this.mutationRate)
					continue;

				T mutant = this.chromosomes[i].Clone();
				mutant.Mutate();
				this.chromosomes.Add(mutant);
				this.mutationsHappened += 1;
			}
		}

		private void PerformCrossover()
		{
			for (int i = 1; i < this.populationSize; i += 2)
			{
				if (random.NextDouble() > this.crossoverRate)
					continue;

				// Perform crossover
				T child1 = this.chromosomes[i - 1].Clone();
				T child2 = this.chromosomes[i].Clone();
				child1.CrossoverWith(child2);

				// Add offspring into population
				this.chromosomes.Add(child1);
				this.chromosomes.Add(child2);

				this.crossoversHappened += 1;
			}
		}

		private void GenerateInitialPopulation(T ancestor)
		{
			this.chromosomes = new List<T>(this.populationSize) { ancestor.Clone() };
			this.chromosomesReadOnly = new ReadOnlyCollection<T>(this.chromosomes);

			for (int i = 1; i < this.populationSize; ++i)
				this.chromosomes.Add(ancestor.CreateRandom());
		}
	}
}
